10  Sistema Input-Output di Java

La libreria standard di Java offre una vasta gamma di classi per la gestione dell’I/O. Queste permettono di gestire tanto un collegamento di rete, quanto un buffer di memoria o un file su disco. Sebbene questi dispositivi siano fisicamente diversi, sono gestiti dalla stessa astrazione: lo stream, una entità logica che produce o consuma informazioni.

Tutti gli stream si comportano ‘logicamente’ allo stesso modo anche se i dispositivi fisici a cui sono collegati sono differenti.

11 Serializzazione

Al termine dell’esecuzione di un programma, i dati utilizzati vengono distrutti. Per poterli preservare fra due esecuzioni consecutive è possibile ricorrere all’uso dell’I/O su file.

La persistenza di un oggetto indica la capacità di un oggetto di poter “vivere” separatamente dal programma che lo ha generato.

Java contiene un meccanismo per creare oggetti persistenti, detto serializzazione: un oggetto viene serializzato trasformandolo in una sequenza di byte che lo rappresentano. In seguito questa rappresentazione può essere usata per ricostruire l’oggetto originale. Una volta serializzato, l’oggetto può essere memorizzato in un file o inviato a un altro computer.

La serializzazione viene realizzata tramite una interfaccia e due classi.

La classe dell’oggetto che si vuole serializzare deve implementare l’interfaccia Serializable, la quale non contiene metodi e serve soltanto al compilatore per comprendere che un oggetto di quella determinata classe può essere serializzato. Per serializzare un oggetto si invoca poi il metodo writeObject della classe ObjectOutputStream; per deserializzarlo si usa il metodo readObject della classe ObjectInputStream.

ObjectInputStream e ObjectOutputStream sono stream di manipolazione e devono essere utilizzati congiuntamente a un OutputStream e un InputStream.

Il compito di InputStream è di rappresentare classi che producono input da diverse sorgenti in forma di stream.

Il compito di OutputStream è di rappresentare classi che producono output negli stessi tipi di sorgenti di InputStream.

FileOutputStream outFile = new FileOutputStream(“info.dat);
ObjectOutputStream outStream = new ObjectOutputStream(outFile);
outStream.writeObject(myCar)

FileOutputStream: serve a inviare informazioni a un file

myCar è un oggetto di una classe Car definita dal programmatore e che implementa l’interfaccia Serializable.

Per poter leggere l’oggetto serializzato e ricaricarlo in memoria centrale si procederà come segue:

FileInputStream inFile = new FileInputStream(“info.dat);
ObjectInputStream inStream = new ObjectInputStream(inFile);
Car myCar = (Car) inStream.readObject();

FileInputStream: serve a leggere informazioni da un file

La serializzazione di un oggetto si occupa di serializzare tutti gli eventuali riferimenti ad esso collegati. Dunque, se la classe Car contenesse dei riferimenti a oggetti di classe Engine, questa verrebbe serializzata automaticamente e diverrebbe parte della serializzazione di Car. La classe Engine dovrà, pertanto, implementare anch’essa l’interfaccia Serializable.

Notare che writeObject prende un Object, quindi se passo un oggetto non serializable non sarà il compilatore a darmi errore ma verrà espulsa una NotSerializableException a runtime.

Se voglio serializzare in modo particolare, mi basta fare l’overloading di writeObject.

Gli attributi static non vengono serializzati. Per poterli salvare occorre provvedere in modo personalizzato.

import java.io.*;  
  
class Nave implements Serializable {  
    private static int nroNavi = 1;  
    private int nroNave;  
    private String nomeNave;  
  
    Nave(String nomeNave) {  
        nroNave = nroNavi++;  
        this.nomeNave = nomeNave;  
    }  
  
    public void salva()
    {
        try {
            FileOutputStream file = new FileOutputStream("info.dat");
            ObjectOutputStream o = new ObjectOutputStream(file);
            o.writeObject(this);
            o.writeObject(nroNavi);
            o.close();
        }
        catch (FileNotFoundException e) {
            System.err.println(e.getMessage());
        }
        catch (IOException e) {
            System.err.println(e.getMessage());
        }
    }

    public static Navi carica() {
        try {
            FileInputStream file = new FileInputStream("info.dat");
            ObjectInputStream o = new ObjectInputStream(file);
            Nave n = (Nave) o.readObject();
            Nave.nroNavi = o.readObject();
            o.close();
            return n;
        } catch (FileNotFoundException e) {
            System.err.println(e.getMessage());
        } catch (IOException e) {
            System.err.println(e.getMessage());
        } catch (ClassCastException e) {
            System.err.println(e.getMessage());
        } catch (ClassNotFoundException e) {
            System.err.println(e.getMessage());
        }
        return null;
    }
}

Molte classi della libreria standard implementano l’interfaccia Serializable in modo da essere serializzate quando necessario. Ovviamente, nel caso di una HashMap ad esempio, anche gli oggetti memorizzati nella struttura dati devono implementare l’interfaccia Serializable.

A volte, quando si serializza un oggetto, si può desiderare di escludere delle informazioni, ad esempio, una password, con la parola chiave transient: questa indica al compilatore di non rappresentarla come parte dello stream di byte della versione serializzata dell’oggetto. Quando deserializzerò l’oggetto leggerò o null o il valore iniziale in corrispondenza dell’attributo transient.